package com.acgist.snail.net.stun;

import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;

import com.acgist.snail.config.PeerConfig;
import com.acgist.snail.config.StunConfig;
import com.acgist.snail.config.SymbolConfig;
import com.acgist.snail.config.SystemConfig;
import com.acgist.snail.context.IContext;
import com.acgist.snail.context.SystemThreadContext;
import com.acgist.snail.logger.Logger;
import com.acgist.snail.logger.LoggerFactory;
import com.acgist.snail.net.NatContext;
import com.acgist.snail.net.torrent.dht.NodeContext;
import com.acgist.snail.utils.NetUtils;
import com.acgist.snail.utils.StringUtils;

/**
 * Stun上下文
 * 
 * @author acgist
 */
public final class StunContext implements IContext {

    private static final Logger LOGGER = LoggerFactory.getLogger(StunContext.class);
    
    private static final StunContext INSTANCE = new StunContext();
    
    /**
     * 配置多个服务器时轮询使用
     */
    private int index = 0;
    /**
     * 服务器地址
     */
    private String server;
    /**
     * 是否启动定时任务
     */
    private volatile boolean scheduled = false;
    /**
     * 是否是否注册成功
     */
    private volatile boolean available = false;
    
    public static final StunContext getInstance() {
        return INSTANCE;
    }
    
    private StunContext() {
    }

    /**
     * @return 是否注册成功
     */
    public boolean available() {
        return this.available;
    }
    
    /**
     * 端口映射
     */
    public void mapping() {
        final InetSocketAddress address = this.buildServerAddress();
        if(address == null) {
            LOGGER.warn("STUN端口映射地址无效");
            return;
        }
        StunClient.newInstance(address).mapping();
    }
    
    /**
     * 设置端口映射信息
     * 
     * @param externalIPAddress 外网IP地址
     * @param port              外网端口
     */
    public void mapping(String externalIPAddress, int port) {
        if(
            !this.available ||
            port != SystemConfig.getTorrentPortExt() ||
            !StringUtils.equals(externalIPAddress, SystemConfig.getExternalIPAddress())
        ) {
            LOGGER.debug("STUN端口映射：{} - {}", externalIPAddress, port);
            this.available = true;
            PeerConfig.enableNat();
            SystemConfig.setTorrentPortExt(port);
            SystemConfig.setExternalIPAddress(externalIPAddress);
            NodeContext.getInstance().buildNodeId(externalIPAddress);
            NatContext.getInstance().unlock();
        } else {
            LOGGER.debug("STUN端口映射（没有变化）：{} - {}", externalIPAddress, port);
        }
        // 开启定时任务：端口保活
        if(!this.scheduled) {
            this.scheduled = true;
            final int interval = SystemConfig.getStunInterval();
            LOGGER.debug("启动STUN端口映射定时服务：{}", interval);
            SystemThreadContext.scheduledAtFixedRate(
                interval,
                interval,
                TimeUnit.SECONDS,
                this::mapping
            );
        }
    }

    /**
     * @return STUN服务器地址
     */
    private InetSocketAddress buildServerAddress() {
        final String server = SystemConfig.getStunServer();
        if(StringUtils.isEmpty(server)) {
            LOGGER.warn("STUN服务器列表格式错误：{}", server);
            return null;
        }
        final String[] servers = SymbolConfig.Symbol.COMMA.split(server);
        if(!this.available) {
            final int index = Math.abs(this.index++ % servers.length);
            this.server     = servers[index];
        }
        return this.buildServerAddress(this.server);
    }
    
    /**
     * 支持STUN服务器地址格式：
     * 
     * stun1.l.google.com
     * stun:stun1.l.google.com
     * stun1.l.google.com:19302
     * stun:stun1.l.google.com:19302
     * 
     * @param server STUN服务器地址列表
     * 
     * @return STUN服务器地址
     */
    private InetSocketAddress buildServerAddress(String server) {
        LOGGER.debug("STUN服务器地址：{}", server);
        final String[] args  = SymbolConfig.Symbol.COLON.split(server);
        final int argLength  = args.length;
        final String lastArg = args[argLength - 1];
        if(argLength == 0) {
            LOGGER.warn("STUN服务器格式错误：{}", server);
            return null;
        } else if(argLength == 1) {
            return NetUtils.buildSocketAddress(lastArg, StunConfig.DEFAULT_PORT);
        } else if(StringUtils.isNumeric(lastArg)) {
            return NetUtils.buildSocketAddress(args[argLength - 2], Integer.parseInt(lastArg));
        } else {
            return NetUtils.buildSocketAddress(lastArg, StunConfig.DEFAULT_PORT);
        }
    }
    
}
